package ucd.csi.comp30220.fall2011.oteop.flight.impl;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import ucd.csi.comp30220.fall2011.oteop.collision.Collision;
import ucd.csi.comp30220.fall2011.oteop.flight.AlreadyStartedException;
import ucd.csi.comp30220.fall2011.oteop.flight.FailedToStartException;
import ucd.csi.comp30220.fall2011.oteop.flight.Flight;
import ucd.csi.comp30220.fall2011.oteop.flight.FlightInfo;
import ucd.csi.comp30220.fall2011.oteop.navigation.GreatCircle;
import ucd.csi.comp30220.fall2011.oteop.navigation.Position;

/**
 * Flights are the main currency of the system. They have a Plane and a private
 * AutoPilot to control it. An Air Traffic Controller gives change of altitude,
 * course instructions to the Flight which passes them on to the AutoPilot.
 * Starting a flight engages the auto pilot which gets the plane to take off,
 * climb to it's cruising altitude and stay there until it is close to the
 * destination at which point it descends and lands.
 * 
 * A few assumptions:
 * 
 * (1) The runway is oriented towards the destination. Handy. 
 * (2) Currently, take-off is instantaneous. The plane goes from zero feet to 
 *     cruising altitude and standstill to cruising speed in the blink of an eye.
 * (3) Same for landing. The plane drops like a stone from cruising to approach
 *     altitude & speed then pancakes on the runway. Glad I'm not a passenger.
 * 
 * @author David Harrison
 */
public class LocalFlight implements Flight, Runnable {

    private FlightInfo info;
    
    private boolean onFinal = false;
    private boolean landed = false;
    
    private List<Position> waypoints;
    private Double trueCourse;
    private double totalDistance;
    
    private int speed; 
    private int altitude; 
    private Position position; 

    private Thread thread = null;
    
    public LocalFlight(FlightInfo info) {
        this.info = info;
    }
    
    public boolean takingOff() {
        return altitude == 0 && speed > 0;
    }
    
    public boolean onFinalApproach() {
        return onFinal;
    }

    public boolean hasLanded() {
        return landed;
    }
    
    public boolean inLevelFlight() {
        return thread != null && thread.isAlive() && 
            !takingOff() && !onFinalApproach();
    }
    
    public FlightInfo getInfo() {
        return info;
    }

    public Position getPosition() {
        return position;
    }

    public double getCourse() {
        return trueCourse;
    }

    public int getSpeed() {
        return speed;
    }

    public int getAltitude() {
        return altitude;
    }

    public void setAltitude(int altitude) {
        this.altitude = altitude;
    }
    
    private boolean avoidingCollision = false;

    public void collisionDetected(Collision collision) {
        if (avoidingCollision) {
            return;
        }
        avoidingCollision = true;
        setAltitude(getAltitude()-2500);
        new Thread(new Runnable() {
            public void run() {
                try { Thread.sleep(15*1000); }
                catch (InterruptedException e) {}
                setAltitude(getAltitude()+2500);
                avoidingCollision = false;
            }
        }).start();
    }

    public void setRoute(Position[] waypoints) {
        stop();
        this.waypoints = new ArrayList<Position>();
        this.waypoints.addAll(Arrays.asList(waypoints));
        trueCourse = GreatCircle.trueCourseBetween(position, this.waypoints.get(0));
        totalDistance = GreatCircle.distanceBetween(position, info.getDestination());
        doStart();
    }
    
    public void setCourse(double course) {
        stop();
        trueCourse = course;
        waypoints = null;
        doStart();
    }
    
    public void start() throws AlreadyStartedException, FailedToStartException {
        if (thread != null)
            throw new AlreadyStartedException();
        try {
            doStart();
        } catch (Throwable t) {
            throw new FailedToStartException(t);
        }
    }
    
    private void doStart() {
        thread = new Thread(this);
        thread.start();
    }
    
    public void stop() {
        thread.interrupt();
    }
    
    public void run() {
        if (waypoints == null && trueCourse == null) {
            getRoute();
            takeOff();
            climb();
        }
        Thread monitor = thread;
        boolean[] interrupted = new boolean[1];
        cruise(monitor, interrupted);
        if (interrupted[0])
            return;
        descend();
        land();
    }

    private void getRoute() {
        totalDistance = GreatCircle.distanceBetween(info.getDeparturePoint(), info.getDestination());
        trueCourse= GreatCircle.trueCourseBetween(info.getDeparturePoint(), info.getDestination());
        int segments = (int) totalDistance / 50; // TODO Smaller segments for shorter trips?
        waypoints = GreatCircle.waypoints(info.getDeparturePoint(), info.getDestination(), segments - 1);
        waypoints.add(info.getDestination());
    }

    /**
     * In the minimum runway length, accelerate evenly to rotate speed
     */
    private void takeOff() {
        // for now, just set rotate speed
        speed = info.getPlaneInfo().getRotateSpeed();
        // Obviously, this will be few hundred feet further on when this method
        // is complete
        position = info.getDeparturePoint();
    }

    /**
     * Climb to cruising altitude at maximum rate of climb
     */
    private void climb() {
        // for now, just set cruise altitude speed
        speed = info.getPlaneInfo().getCruisingSpeed();
        altitude = info.getPlaneInfo().getCruisingAltitude();
        // Obviously, this will be few miles further on when this method is complete
        position = info.getDeparturePoint();
    }

    /**
     * Navigate towards destination by flying from waypoint to waypoint
     */
    private void cruise(final Thread monitor, final boolean[] interrupted) {
        Position current = position;
        long time = System.currentTimeMillis();
        long ctm;
        double travelled;
        long sleepMillis = Math.max(10, 1000/(Math.max(1,speed/400)));
        if (waypoints != null) {
            for (Position waypoint : waypoints) {
                if (interrupted[0]) {
                    break;
                }
                trueCourse = GreatCircle.trueCourseBetween(current, waypoint);
                double length = GreatCircle.distanceBetween(current, waypoint);
                double distance = 0.0;
                while (distance < length) {
                    if (monitor.isInterrupted()) {
                        interrupted[0] = true;
                        break;
                    }
                    try { Thread.sleep(sleepMillis);
                    } catch (InterruptedException e) { 
                        interrupted[0] = true;
                        break;
                    }
                    ctm = System.currentTimeMillis();
                    travelled = (((double) speed) / 
                        ((double) (60 * 60 * 1000))) * ((double) (ctm - time));
                    current = GreatCircle.destinationFrom(current, trueCourse, travelled);
                    distance += travelled;
                    time = ctm;
                    position = current;
                    if (GreatCircle.distanceBetween(current, info.getDestination()) < 5)
                        break;
                }
            }
        } else {
            while (true) {
                if (monitor.isInterrupted()) {
                    interrupted[0] = true;
                    break;
                }
                try { Thread.sleep(sleepMillis);
                } catch (InterruptedException e) { 
                    interrupted[0] = true;
                    break;
                }
                ctm = System.currentTimeMillis();
                travelled = (((double) speed) / 
                    ((double) (60 * 60 * 1000))) * ((double) (ctm - time));
                current = GreatCircle.destinationFrom(current, trueCourse, travelled);
                position = current;
                if (GreatCircle.distanceBetween(current, info.getDestination()) < 5)
                    break;
            }
        }
    }

    /**
     * Descend (at maximum rate of climb) to approach altitude/speed
     */
    private void descend() {
        // Bit bonkers, but ok for now
        position = info.getDestination();
        // for now, just set appropriate values
        speed = info.getPlaneInfo().getApproachSpeed();
        // Altitude should come from airport control;
        altitude = 500;
    }

    /**
     * Slow and, erm, put the thing down!
     */
    private void land() {
        // This needs considerable improvement :)
        position = info.getDestination();
        onFinal = true;
        // for now, just set the speed/altitude to zero
        position = info.getDestination();
        speed = 0;
        altitude = 0;
        onFinal = false;
        landed = true;
    }
}
